use actix_multipart::Multipart;
use actix_web::{delete, error, get, post, put, web, Error, HttpResponse, Result};
use chrono::prelude::*;
use futures::{StreamExt, TryStreamExt};
use std::{collections::HashMap, vec};
use std::{fs, io::Write};
use uuid::Uuid;

use crate::{
    controller::Res,
    dao::{tutorials, types},
    models::{NewTutorial, TreeNode, Tutorial, Type},
    utils, DbPool,
};

#[get("/article/")]
pub async fn list(
    tmpl: web::Data<tera::Tera>,
    query: web::Query<HashMap<String, String>>,
    pool: web::Data<DbPool>,
) -> Result<HttpResponse, Error> {
    let conn = pool.get().expect("couldn't get db connection from pool");
    let mut ctx = tera::Context::new();
    if let Some(id) = query.get("tid") {
        if let Ok(ret) = tutorials::get_by_tid(id.clone(), true, true, &conn) {
            let tutorials = TreeNode::<Tutorial>::new(ret);
            ctx.insert("tutorials", &tutorials);
        }
    }

    let s = tmpl
        .render("admin/article/list.html", &ctx)
        .map_err(|_| error::ErrorInternalServerError("Template error"))?;
    Ok(HttpResponse::Ok().content_type("text/html").body(s))
}

#[post("/article/upload")]
pub async fn upload(mut payload: Multipart) -> Result<HttpResponse, Error> {
    let allow_ext = vec![
        "png", "jpg", "jpeg", "gif", "bmp", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx",
        "txt", "md", "zip", "rar", "7z", "mp3", "mp4",
    ];

    let mut err_msg = String::new();
    // iterate over multipart stream
    while let Ok(Some(mut field)) = payload.try_next().await {
        let content_type = field.content_disposition().unwrap();
        let filename = content_type.get_filename().unwrap();
        let mut vs: Vec<&str> = filename.split(".").collect();
        vs.reverse();

        // 判断
        if vs.len() < 2 {
            err_msg = "上传的文件没有后缀名".to_string();
            break;
        }
        let ext = vs.get(0).unwrap();
        // 判断是否是允许上传的文件后缀
        let mut is_ext_allow = false;
        for v in allow_ext {
            if ext.to_string() == v {
                is_ext_allow = true;
            }
        }
        if !is_ext_allow {
            err_msg = format!("不支持上传 [{}] 后缀的文件", ext);
            break;
        }

        let uuid = Uuid::new_v4();

        // 生成年月，作为目录的一部分
        let dt = Utc::now().with_timezone(&FixedOffset::east(8 * 3600));
        let year_mouth = format!("{}/{}", dt.year(), dt.month());
        // 上传路径
        let upload_dir = format!("./static/upload/{}", year_mouth);
        // 判断上传的目录是否存在,不存在就创建
        if !utils::dir_exists(upload_dir.as_str()) {
            fs::create_dir_all(upload_dir.as_str()).expect(("没有权限创建目录：".to_string() + upload_dir.as_str()).as_str())
        }

        // 上传的文件全路径
        let filepath = format!("{}/{}.{}", upload_dir, uuid, ext);
        // 提供web访问的url
        let pubpath = format!("/upload/{}/{}.{}", year_mouth, uuid, ext);
        // File::create is blocking operation, use threadpool
        let mut f = web::block(|| std::fs::File::create(filepath))
            .await
            .unwrap();

        // Field in turn is stream of *Bytes* object
        while let Some(chunk) = field.next().await {
            let data = chunk.unwrap();
            // filesystem operations are blocking, we have to use threadpool
            f = web::block(move || f.write_all(&data).map(|_| f)).await?;
        }

        // 记录是否是图片，默认不是图片
        let mut is_img = false;
        for v in ["png", "jpg", "jpeg", "gif", "bmp"].iter() {
            if v == ext {
                is_img = true;
                break;
            }
        }
        let ret = format!(
            r#"{{"code":0,
    "src":"{}",
     "alt":"{}", 
     "isImage":{},
     "errMsg":""}}"#,
            pubpath, filename, is_img
        );
        return Ok(ret.into());
    }
    let ret = format!(r#"{{"code":1,"errMsg":"{}"}}"#, err_msg);
    Ok(HttpResponse::Ok().json(ret))
}

#[get("/article/add")]
pub async fn add_page(
    tmpl: web::Data<tera::Tera>,
    pool: web::Data<DbPool>,
    query: web::Query<HashMap<String, String>>,
) -> Result<HttpResponse, Error> {
    // submitted form
    let mut ctx = tera::Context::new();
    let conn = pool.get().expect("couldn't get db connection from pool");
    let ret = types::get_all(true, &conn).unwrap();
    let nodes = TreeNode::<Type>::new(ret);

    if let Some(id) = query.get("add") {
        if let Ok(add_type) = types::get(id.clone(), &conn) {
            ctx.insert("add_type", &add_type);
        }
    }
    ctx.insert("types", &nodes);

    let s = tmpl
        .render("admin/article/add.html", &ctx)
        .map_err(|e| error::ErrorInternalServerError(e))?;
    Ok(HttpResponse::Ok().content_type("text/html").body(s))
}

#[get("/article/edit")]
pub async fn edit_page(
    tmpl: web::Data<tera::Tera>,
    pool: web::Data<DbPool>,
    query: web::Query<HashMap<String, String>>,
) -> Result<HttpResponse, Error> {
    // submitted form
    let mut ctx = tera::Context::new();
    let conn = pool.get().expect("couldn't get db connection from pool");
    let ret = types::get_all(true, &conn).unwrap();
    let nodes = TreeNode::<Type>::new(ret);

    if let Some(id) = query.get("edit") {
        if let Ok(tutorial) = tutorials::get(id.clone(), true, &conn) {
            ctx.insert("tutorial", &tutorial);
        }
    }
    ctx.insert("types", &nodes);

    let s = tmpl
        .render("admin/article/edit.html", &ctx)
        .map_err(|e| error::ErrorInternalServerError(e))?;
    Ok(HttpResponse::Ok().content_type("text/html").body(s))
}

#[post("/article/add")]
pub async fn add(new_tutorial: web::Json<NewTutorial>, pool: web::Data<DbPool>) -> HttpResponse {
    let conn = pool.get().expect("couldn't get db connection from pool");
    let ret = tutorials::insert(&conn, new_tutorial.0).unwrap();
    HttpResponse::Ok().json(Res::new(Some(ret)))
}

#[put("/article/edit")]
pub async fn edit(new_tutorial: web::Json<NewTutorial>, pool: web::Data<DbPool>) -> HttpResponse {
    let conn = pool.get().expect("couldn't get db connection from pool");
    let ret = tutorials::update(&conn, new_tutorial.0).unwrap();
    HttpResponse::Ok().json(Res::new(Some(ret)))
}

#[get("/article/by_tid")]
pub async fn get_all_by_tid(
    pool: web::Data<DbPool>,
    query: web::Query<HashMap<String, String>>,
) -> HttpResponse {
    let conn = pool.get().expect("couldn't get db connection from pool");

    let res;
    if let Some(id) = query.get("tid") {
        if let Ok(nodes) = tutorials::get_by_tid(id.into(), false, true, &conn) {
            let mut ret = vec![];
            let mut data = HashMap::<&str, String>::new();
            for v in nodes {
                data.insert("id", v.id);
                data.insert("pid", v.pid.unwrap_or("".into()));
                data.insert("tid", v.tid);
                data.insert("title", v.title);
                ret.push(data.clone());
            }
            res = Res::new(Some(ret));
            return HttpResponse::Ok().json(res);
        } else {
            return HttpResponse::Ok().json(Res::<String>::new_with_error(
                1,
                "获取文章列表出错".to_string(),
            ));
        }
    }
    return HttpResponse::Ok().json(Res::<String>::new_with_error(0, "没有文章".to_string()));
}

#[delete("/articles/")]
pub async fn delete(
    query: web::Query<HashMap<String, String>>,
    pool: web::Data<DbPool>,
) -> HttpResponse {
    let conn = pool.get().expect("couldn't get db connection from pool");

    let res;
    if let Some(id) = query.get("delete") {
        if let Ok(count) = tutorials::count_children(id.clone(), &conn) {
            if count > 0 {
                return HttpResponse::Ok().json(Res::<String>::new_with_error(
                    3,
                    "该文章下存在子文章，请先删除所有子文章".to_string(),
                ));
            }
        } else {
            return HttpResponse::Ok().json(Res::<String>::new_with_error(
                4,
                "获取子文章出错".to_string(),
            ));
        }

        if let Ok(ret) = tutorials::delete(id.clone(), &conn) {
            res = Res::new(Some(ret));
            return HttpResponse::Ok().json(res);
        } else {
            return HttpResponse::Ok()
                .json(Res::<String>::new_with_error(1, "删除文章出错".to_string()));
        }
    } else {
        return HttpResponse::Ok().json(Res::<String>::new_with_error(
            2,
            "请在 url 中带上要删除的文章 id".to_string(),
        ));
    }
}
